Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 2da7d5374b7cdfab1d4442b5f946275799009fe6


Parents : d5c0b82
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-16T16:50:34-05:00

fix: prevent reentrant logging errors during shutdown

Changes
Diff

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fec15f20..2a89b7b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,7 @@ All notable changes to this project will be documented in this file.
- Unread badge stays circular and remains visible when the sidebar is collapsed
- Open conversations mark as read when a new message arrives without needing to reselect the thread
- Startup stage logs no longer print the same stage twice
+- Ctrl+C shutdown no longer floods reentrant logging errors from RNS.exit containment
## [4.7.2] - 2026-07-06

diff --git a/meshchatx.rsm b/meshchatx.rsm
index c91c72c7..01d3fde3 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/meshchatx/src/backend/persistent_log_handler.py b/meshchatx/src/backend/persistent_log_handler.py
index b25a93a3..4949ef2f 100644
--- a/meshchatx/src/backend/persistent_log_handler.py
+++ b/meshchatx/src/backend/persistent_log_handler.py
@@ -45,6 +45,11 @@ class PersistentLogHandler(logging.Handler):
self.database = database
def emit(self, record):
+ # Nested emit (e.g. signal handler during a flush) must not recurse into
+ # SQLite or other handlers via handleError.
+ if getattr(self, "_emitting", False):
+ return
+ self._emitting = True
try:
msg = self.format(record)
timestamp = datetime.now(UTC).timestamp()
@@ -77,7 +82,12 @@ class PersistentLogHandler(logging.Handler):
self._flush_to_db()
except Exception:
- self.handleError(record)
+ try:
+ self.handleError(record)
+ except Exception:
+ pass
+ finally:
+ self._emitting = False
def _detect_access_anomaly(self, message):
"""Detect anomalies in aiohttp access logs."""

diff --git a/meshchatx/src/backend/rns_startup_recovery.py b/meshchatx/src/backend/rns_startup_recovery.py
index eda5ab92..35e47839 100644
--- a/meshchatx/src/backend/rns_startup_recovery.py
+++ b/meshchatx/src/backend/rns_startup_recovery.py
@@ -17,6 +17,7 @@ from __future__ import annotations
import logging
import os
import re
+import contextlib
from collections.abc import Callable
from typing import Any
@@ -26,6 +27,7 @@ _TRUE_STRINGS = ("true", "yes", "1", "on", "y")
_PANIC_PATCHED = False
_ORIGINAL_PANIC = None
_ORIGINAL_EXIT = None
+_EXIT_IN_PROGRESS = False
# Prefer disabling these types first when init fails without a named culprit.
_HIGH_RISK_TYPES = (
@@ -51,7 +53,7 @@ def install_rns_panic_containment(*, force: bool = False) -> bool:
Safe to call multiple times. Returns True when the patch was applied (or
was already applied).
"""
- global _PANIC_PATCHED, _ORIGINAL_PANIC, _ORIGINAL_EXIT
+ global _PANIC_PATCHED, _ORIGINAL_PANIC, _ORIGINAL_EXIT, _EXIT_IN_PROGRESS
if _PANIC_PATCHED and not force:
return True
try:
@@ -60,6 +62,9 @@ def install_rns_panic_containment(*, force: bool = False) -> bool:
logger.warning("Could not import RNS for panic containment: %s", exc)
return False
+ if force:
+ _EXIT_IN_PROGRESS = False
+
if _ORIGINAL_PANIC is None:
_ORIGINAL_PANIC = getattr(RNS, "panic", None)
if _ORIGINAL_EXIT is None:
@@ -69,19 +74,25 @@ def install_rns_panic_containment(*, force: bool = False) -> bool:
message = "RNS.panic() was called"
if _args:
message = f"RNS.panic(): {_args[0]}"
- logger.error(message)
+ # Avoid logging handlers here. Panic can run under signal context.
raise RnsPanicError(message)
def _contained_exit(code: int = 0):
- message = f"RNS.exit({code}) was called"
- logger.error(message)
+ global _EXIT_IN_PROGRESS
+ # SIGINT/SIGTERM can reenter while logging or SQLite is in flight.
+ # A second RNS.exit must be a no-op or FileHandlers blow up with
+ # "reentrant call inside BufferedWriter".
+ if _EXIT_IN_PROGRESS:
+ return
+ _EXIT_IN_PROGRESS = True
try:
if hasattr(RNS, "Reticulum") and hasattr(RNS.Reticulum, "exit_handler"):
- RNS.Reticulum.exit_handler()
- except Exception as exc:
- logger.warning("RNS exit_handler during contained exit failed: %s", exc)
- if code != 0:
- raise RnsPanicError(message)
+ with contextlib.suppress(Exception):
+ RNS.Reticulum.exit_handler()
+ finally:
+ if code != 0:
+ _EXIT_IN_PROGRESS = False
+ raise RnsPanicError(f"RNS.exit({code}) was called")
RNS.panic = _contained_panic
RNS.exit = _contained_exit

diff --git a/tests/backend/test_rns_startup_recovery.py b/tests/backend/test_rns_startup_recovery.py
index d0bd8429..e2b74821 100644
--- a/tests/backend/test_rns_startup_recovery.py
+++ b/tests/backend/test_rns_startup_recovery.py
@@ -28,6 +28,30 @@ def test_install_rns_panic_containment_raises_instead_of_exit(monkeypatch):
assert calls["exit"] == 0
+def test_contained_exit_is_reentrant_safe(monkeypatch):
+ import RNS
+
+ recovery._EXIT_IN_PROGRESS = False
+ assert recovery.install_rns_panic_containment(force=True) is True
+ handler_calls = {"n": 0}
+
+ def counting_exit_handler():
+ handler_calls["n"] += 1
+ # Nested exit must not recurse or raise.
+ RNS.exit(0)
+
+ monkeypatch.setattr(
+ RNS.Reticulum,
+ "exit_handler",
+ staticmethod(counting_exit_handler),
+ raising=False,
+ )
+ RNS.exit(0)
+ assert handler_calls["n"] == 1
+ RNS.exit(0)
+ assert handler_calls["n"] == 1
+
+
def test_ensure_panic_on_interface_error_disabled(tmp_path):
config_path = tmp_path / "config"
config_path.write_text(


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────